◆自己紹介
MS事業部データマイニング推進部 ミシェルです。
◆SIGNATEとは
企業が問題解決のためにデータを提供し、エンジニアがデータを用いて
予測モデルを作成し精度を競い合うコンペの開催など行われるデータ分析のコミュニティ。
◆予測モデルとは
機械学習アルゴリズム(データを処理するアルゴリズム)が
大量のデータの中から関係性を見つけだし、計算式の塊を作る。
この計算式の塊を予測モデルと呼び、ここに新たなデータを当てはめることで予測が出来る。
◆コンペ概要
ある会社のカフェフロアで販売されているお弁当の売上数を予測するモデルを作成する。
曜日やメニュー等の複数の変数から最適なお弁当の販売量を予測し、お弁当の売上向上・廃棄削減につなげる。
◆教師あり学習
正解となるデータが存在する場合に用いられる手法。
・教師あり学習で解ける問題は大きく分けて「分類」と「回帰」がある。
分類:「動物の種類」や「YesかNoか」などデータが属するクラスを予測する問題
回帰:「年齢」や「金額」などの連続量、数値を予測する問題
今回は、お弁当の売上数を予測する問題なので回帰問題である。
◆データ
データ期間:お弁当の販売を開始した 2013年11月18日 から 2014年11月30日 まで(土日祝を除く平日)(247日)
学習用データ期間:2013年11月18日 ~ 2014年 9月30日
検証用データ期間:2014年10月 1日 ~ 2014年11月30日
◆評価指標
・モデル精度の評価は、評価関数「RMSE(Root Mean Squared Error 平均二乗平方根誤差)」を使用する。
・予測値が正解とどれくらい乖離しているか示すもので、値が小さいほど精度が高いと判断する。
◆分析の手順
①前処理(欠損処理など学習しやすくするためのデータの加工)
②モデル作成(パラメータチューニングなど)
③評価
import pandas as pd
import gc
from sklearn.ensemble import RandomForestRegressor
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import KFold,cross_val_score,GridSearchCV
from matplotlib import pyplot as plt
import seaborn as sns
sns.set(font="IPAexGothic",style="white")
from sklearn.metrics import mean_squared_error as MSE
from sklearn.preprocessing import StandardScaler
import numpy as np
from IPython.display import Image, display_png
# 現在の最大表示列数の出力
pd.get_option("display.max_columns")
# 最大表示列数の指定(ここでは50列を指定)
pd.set_option('display.max_columns', 50)
日付のカラムをインデックスとして取り込み
train = pd.read_csv('./data/train.csv',index_col=['datetime'])
test = pd.read_csv('./data/test.csv',index_col=['datetime'])
00 datetid(datetime):インデックスとして使用する日付(yyyy-m-d)
01 y(int):販売数(目的変数)
</div>
02 week(char):曜日(月~金)
03 soldout(boolean):完売フラグ(0:完売せず、1:完売)
04 name(varchar):メインメニューの名前
05 kcal(int):おかずのカロリー
06 remarks(varchar):特記事項
07 event(varchar):13時開始お弁当持ち込み可の社内イベント
08 payday(boolean):給料日フラグ(1:給料日)
09 weather(varchar):天気
10 precipitation(float):降水量。ない場合は "--"
11 temperature(float):気温
train.tail(20)
test.head()
# (行数,カラム数)
train.shape
# (行数,カラム数)
test.shape
trainデータとtestデータ両方に前処理を適用させるため、結合する。
モデリング前にtrainデータとtestデータに分けられるようにフラグを立てておく。
# フラグを立てる
train['flg'] = 1
test['flg'] = 0
# trainデータとtestデータを結合
all_data = pd.concat([train,test], axis=0, sort=False)
# 欠損数、型確認
all_data.info()
# 各カラムの統計量を見る
all_data.describe()
欠損値があると解析が難しくなるため、除去または補完する。
◆kcal(カロリー)
・2013-12-26までカロリーを記録していない
all_data.loc[all_data['kcal'].isnull() , ['week', 'kcal','y']]
・2013-12-26以降、「お楽しみメニュー」の日は取ってない
all_data.loc[all_data['week'] == '金' , ['week', 'kcal', 'remarks','y']]
・大きくずれた値がないので平均値で埋める
all_data['kcal'].describe()
all_data['kcal'] = all_data['kcal'].fillna(train['kcal'].mean())
all_data['kcal'].head()
◆payday(給料日フラグ(1:給料日))
・給料日は月一
print(train.payday.unique())
all_data.loc[~all_data['payday'].isnull() , ['week', 'payday']]
・給料日=1なので、nanは0で埋める
all_data['payday'] = all_data['payday'].fillna(0)
all_data['payday'].unique()
◆event(13時開始お弁当持ち込み可の社内イベント)
all_data['event'] = all_data['event'].fillna('なし')
all_data['event'].unique()
◆remarks(お楽しみメニューなど特記事項の有無)
目的変数と説明変数の関係を箱ひげ図で確認(カテゴリ変数のみ)
顕著に売上数に反応しているのはremarksの値がお楽しみメニューである時
fig, ax = plt.subplots(2,2,figsize=(12,7))
sns.boxplot(x="week",y="y",data=train,ax=ax[0][0])
sns.boxplot(x="weather",y="y",data=train,ax=ax[0][1])
sns.boxplot(x="remarks",y="y",data=train,ax=ax[1][0])
ax[1][0].set_xticklabels(ax[1][0].get_xticklabels(),rotation=30)
sns.boxplot(x="event",y="y",data=train,ax=ax[1][1])
plt.tight_layout()
お楽しみメニューでないときは「なし」で埋める
all_data.loc[all_data['remarks'] != 'お楽しみメニュー', 'remarks'] = 'なし'
all_data['remarks'].unique()
欠損が埋まったか確認
all_data.info()
◆precipitation(降水量)
'--'という文字列と数値が混在しているため、
このままでは量的変数にもカテゴリ変数にも使えない
all_data[['weather', 'precipitation']].tail(30)
・カテゴリ変数'precipitation_flg'を作る
降水量が--の時、「快晴、曇、晴れ、薄曇」→「天気が良い」とみなし'良い'で埋める
降水量が0の時、「雨、曇、雷電、薄曇」→小雨等「あまり天気が良くない」とみなし'やや悪い'で埋める
降水量に数値が入っている時、「曇、雨、雪」→「天気が悪い」とみなし'悪い'で埋める
# 降水量が--の時
all_data[all_data['precipitation'] == "--"]['weather'].unique()
# 降水量が0の時
all_data[all_data['precipitation'] == "0"]['weather'].unique()
# 降水量に数値が入っている時
all_data[~(all_data['precipitation'] == '0') & ~(all_data['precipitation'] == '--')]['weather'].unique()
# precipitation_flg(カテゴリ変数)
all_data['precipitation_flg'] = '悪い'
all_data.loc[(all_data['precipitation'] == '--'), 'precipitation_flg'] = '良い'
all_data.loc[(all_data['precipitation'] == '0'), 'precipitation_flg'] = 'やや悪い'
・'--'を数値に変換して量的変数として使う
# 文字列を数値データに変える(量的変数)
all_data['precipitation'] = all_data['precipitation'].replace('--', '-1')
◆経過日数カラム
お弁当の売り上げ分布の確認
日数が経過するにつれて売り上げが落ちていく傾向=線形な関係がある
ただし所々急増するところがあるため為、売り上げ数に寄与している何かしらの要因がある
train["y"].plot(figsize=(15,4))
日付の経過と関係が深いため、連番の数値を入れた経過日数カラムを作る
# 経過日数カラムを作成
all_data['days'] = range(1,len(all_data)+1)
カテゴリデータをOhe-Hot表現にする。
適用出来るカラムは
・一意の値が少ないもの
・カテゴリ変数であるもの(=量的変数でないもの)
「曜日,売り切れフラグ,特記事項,イベント,給料日,天気,降水量フラグ」をOne-Hotする
# 各カラムの一意の値を見る
for col in all_data.columns:
print(col,all_data[col].unique())
print(all_data[col].nunique())
print('----------------------------------------------')
all_data[['week']].head(10)
# One-Hotエンコーダー
ohe_columns = ['week','soldout','remarks','event','payday','weather','precipitation_flg']
all_data = pd.get_dummies(all_data, dummy_na=False, columns=ohe_columns)
all_data[['week_月','week_火','week_水','week_木','week_金',]].head(10)
# trainとtestにわける
train = all_data.query('flg == 1')
test = all_data.query('flg == 0')
捨てるカラム
・結合した際testデータに作られたyカラム
・結合に使ったflgカラム
・一意の値が多く、One-Hot出来ないnameカラム
train.drop(['flg','name'], axis=1,inplace=True)
test.drop(['flg','name','y'], axis=1,inplace=True)
train.head()
◆線形回帰とは
目的変数と説明変数の関係を直線で表せるとみなした回帰モデル
display_png(Image("./pic/linear.png", width=300, height=200,))
・説明変数と目的変数を分ける
・説明変数に使うカラムを厳選する
※チュートリアルでは'days'しか説明変数にしていない。
今回は徐々にカラムを増やし効果のあるものだけを追加した。
# trainデータの説明変数と目的変数をわける
linear_X_train = train[["days", "temperature", "kcal", "remarks_お楽しみメニュー", "remarks_なし"
,"week_月", "week_火", "week_水", "week_木", "week_金", "weather_雨","soldout_1"
,"precipitation_flg_やや悪い","precipitation_flg_悪い"]]
# 目的変数はLinear,RandomForest共通
y_train = train['y']
# testデータもlinear_X_trainと同じカラムにする
linear_test = test[["days", "temperature", "kcal", "remarks_お楽しみメニュー", "remarks_なし"
,"week_月", "week_火", "week_水", "week_木", "week_金", "weather_雨","soldout_1"
,"precipitation_flg_やや悪い","precipitation_flg_悪い"]]
LinearRegressionのパラメータ設定
今回はチュートリアルに沿って、デフォルトに設定
# LinearRegressionのパラメータ設定
linear = LinearRegression()
モデルの作成
# 線形回帰モデル作成
linear.fit(linear_X_train,y_train)
# 予測
pred = linear.predict(linear_X_train)
RMSE算出
sklearnにはRMSEが実装されていない為、MSEにルートをとり、導出する
# y_trainと予測値のRMSEを出す
print("RMSE",np.sqrt(MSE(y_train,pred)))
# 線形回帰の予測値と実数値のグラフ
p = pd.DataFrame({"actual":y_train,"pred":pred})
p.plot(figsize=(15,4))
LinearRegressionで予測した結果の残差を目的変数として学習し、
予測値を補正する為の非線形なモデルとしてRandomForestを使う。
◆RandomForestとは
複数の決定木からできたアルゴリズム
display_png(Image("./pic/RF_1.png", width=300, height=200,))
回帰では、それぞれの結果の平均を取る
display_png(Image("./pic/RF_2.png", width=300, height=200,))
◆パラメータ
・n_estimators=木の数(デフォルト:10)
・max_depth=木の深さ(デフォルト:なし)
・random_state=シード値を固定
今回はチュートリアルと同じ数値に設定
# RandomForestのパラメータ設定
randomforest = RandomForestRegressor(n_estimators=100,max_depth=4,random_state=2018)
# 残差 = 実測値 - 予測値 を求める
pred_sub = y_train - pred
# RandomForestの説明変数には全てのカラムを使う
randomforest_X_train = train.drop(['y'],axis=1)
# randomforest_X_trainを説明変数、pred_sub(残差)を目的変数にし、モデル作成
randomforest.fit(randomforest_X_train,pred_sub)
# 線形回帰の予測値に、RandomForestで予測した残差を足し合わせたものを、最終的な予測値とする
pred = linear.predict(linear_X_train) + randomforest.predict(randomforest_X_train)
# アンサンブル学習後のRMSE
print("RMSE",np.sqrt(MSE(y_train,pred)))
# アンサンブル学習後の予測値と実数値のグラフ
p = pd.DataFrame({"actual":y_train,"pred":pred})
p.plot(figsize=(15,4))